require 'net/smtp'

# Symbol extensions
class Symbol
  # Returns the symbol as a header string
  def to_header
    to_s.split('_').map{|w| w.capitalize}.join('-')
  end
end

class Array
  def each_slice(bunch_size)
    a = 0
    bunch = inject([]) do |b, i|
      a += 1
      if b.size < bunch_size
        b << i
      else
        yield b
        b = [i]
      end
      b
    end
    yield bunch unless bunch.empty?
  end

  def shuffle
    sort_by { rand }
  end
end

class String
  # Returns self
  def to_header
    self
  end
  
  # Returns the address part of a recipient
  def email_addr
    s = strip
    return nil if s.empty?
    s =~ /<(.*)>/ ? $1 : s
  end
end

class BulkMail
  @@server_settings = {
    :host => 'localhost',
    :port => 25,
    :helo_domain => nil,
    :user => nil,
    :password => nil,
    :auth => :plain
  }

  # Returns the server_settings hash
  def self.server_settings
    @@server_settings
  end
  
  LINE_BREAK = "\r\n"
  HEADER_FMT = "%s: %s\r\n".freeze
  
  # Returns the message headers formatted
  def self.format_headers(headers)
    headers.inject('') do |m, kv|
      m << (HEADER_FMT % [kv[0].to_header, kv[1]])
    end
  end
  
  # Starts an SMTP session and yields
  def self.smtp
    Net::SMTP.start(
      @@server_settings[:host], 
      @@server_settings[:port], 
      @@server_settings[:helo_domain] || @@server_settings[:host],
      @@server_settings[:user], 
      @@server_settings[:password], 
      @@server_settings[:auth]) do |smtp|
      yield smtp
    end
  end
  
  # Sends an email
  def self.send_message(smtp, from, to, body, headers = nil)
    if headers && !headers.empty?
      msg = format_headers(headers)
    else
      msg = LINE_BREAK
    end
    msg << LINE_BREAK
    msg << body
    smtp.send_message msg, from, to
  end
  
  # Sends bulk mail to a large number of recipients.
  def self.send_bulk(sender, recipients, body, headers)
    failed = []
    succeeded = []
#    headers[:from] = headers[:to] = sender
#    headers[:from] = sender
    recipients = recipients.shuffle.map {|r| r ? r.email_addr : nil}.compact
    recipients.each do |addr|
      headers[:to] = addr
      sent = false
      error = nil
      done = false
      t = Thread.new do
        begin
          smtp {|s| send_message(s, sender.email_addr, addr, body, headers)}
          sent = true
        rescue => e
          error = e
        ensure
          done = true
        end
      end
      while !done; sleep 0.1; end
      if sent
        succeeded << addr
        puts "Sent message to #{addr}"
        File.open('sent.bulkmail', 'a') {|f| f.puts addr}
      else
        failed << addr
        puts "Failed to send to #{addr}: #{error ? error.message : "timeout"}"
      end
      puts "zzz"
      sleep(@@server_settings[:delay] || 60)
    end
    # log failed addresses
    unless failed.empty?
      File.open('failed.bulkmail', 'a') {|f| failed.each {|l|f.puts l}}
    end
    puts "Message to sent to #{succeeded.size} of #{recipients.size} recipients"
  end
end
